import 'dart:convert';

import 'package:my_flutter_core/config/general_config.dart';
import 'package:my_flutter_core/extensions/string_extension.dart';
import 'package:my_flutter_core/models/token.dart';
import 'package:my_flutter_core/models/user.dart';
import 'package:my_flutter_core/services/local_storage.dart';
import 'package:my_flutter_core/utils/date_time_utils.dart';

/// Manages the access token and refresh token.
/// Saves, loads, removes token. Checks token expiration. And so on.
class TokenService {
  // Cached token which loaded from local.
  static Token? _token;

  /// Gets current token. If the cached token does not exist we need to load it from local.
  static Future<Token?> get token async {
    if (_token == null) {
      await _loadToken();
    }

    return _token;
  }

  /// Checks if the access token exists.
  static Future<bool> get hasAccessToken async {
    return (await activeAccessToken).isNotNullOrEmpty;
  }

  /// Gets current access token. It might be null.
  static Future<String?> get activeAccessToken async {
    return (await token)?.accessToken;
  }

  /// Checks if the refresh token exists.
  static Future<bool> get hasRefreshToken async {
    return (await activeRefreshToken).isNotNullOrEmpty;
  }

  /// Gets current refresh token. It might be null.
  static Future<String?> get activeRefreshToken async {
    return (await token)?.refreshToken;
  }

  /// Gets current token bearer. It might be null.
  static Future<String?> get bearer async {
    return (await token)?.bearer;
  }

  /// Checks if current access token expired.
  static Future<bool> get isAccessTokenExpired async {
    // We check on [dateRetrieved] instead of [dateExpired] for below reason.
    // Consider the app might be used in different time zone with server.
    // With this circumstance check on server [dateExpired] value will be inappropriate.
    return (await token)!.localDateExpired!.isBefore(getCurrentDateTime());
  }

  /// Saves the [token] on local and cache it.
  static Future<void> saveToken(Token token) async {
    // Updates the cached token.
    _token = token;

    // Converts the [token] to json format.
    final tokenInfo = token.toJson();

    // Encodes the json data to json format string.
    final encodedData = json.encode(tokenInfo);

    await LocalStorage.set(
      GeneralConfig.localAccessTokenStorageKey,
      encodedData,
    );
  }

  /// Removes token from local and clear the cached token.
  static Future<void> removeToken() async {
    _token = null;
    await LocalStorage.remove(GeneralConfig.localAccessTokenStorageKey);
  }

  /// Gets the key for the map to identify each user.
  ///
  /// It is used for saving user info collection and access token in local secure storage.
  static String getMapKey(User? user) {
    return 'token-${user?.id}';
  }

  /// Gets the token for [user] in collection. This is for quick login feature.
  static Future<Token?> getTokenForUserInCollection(User user) async {
    Token? token;

    // Reads the token info from local secure storage as string format.
    final tokenString = await LocalStorage.get<String>(getMapKey(user));

    if (tokenString.isNotNullOrEmpty) {
      // Decodes the string to json object.
      final decodedToken = json.decode(tokenString!);

      // Converts the [Map<String, dynamic>] to [Token].
      token = Token.fromJson(decodedToken);
    }

    return token;
  }

  /// Saves the current [user]'s [token] in local secure storage to enable quick login feature.
  static void saveTokenForUserInCollection(User user) async {
    // Encodes the json data to json format string.
    final encodedData = json.encode(await token);

    // Saves the encoded json format string on local.
    LocalStorage.set(getMapKey(user), encodedData);
  }

  /// Removes the [user]'s token from local secure storage to disable quick login feature.
  static void removeTokenForUserInCollection(User user) {
    LocalStorage.remove(getMapKey(user));
  }

  /// Loads token from local and cache it.
  static Future<void> _loadToken() async {
    // Loads the token data from local with string format.
    final tokenString = await LocalStorage.get<String>(
      GeneralConfig.localAccessTokenStorageKey,
    );

    if (tokenString.isNotNullOrEmpty) {
      // Decodes the string to json object.
      final tokenDecoded = json.decode(tokenString!);

      // Sets the [_token] property.
      _token = Token.fromJson(tokenDecoded);
    }
  }
}
